package com.joymedia.thor.page;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.io.Serializable;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;

/**
 * @author XSH
 */
public class Sort implements Serializable {

    public static final Direction DEFAULT_DIRECTION = Direction.ASC;
    private static final boolean DEFAULT_IGNORE_CASE = false;
    private static final NullHandling DEFAULT_NULL_HANDLING = NullHandling.NATIVE;

    private final Direction direction;
    private final String property;
    private final boolean ignoreCase;
    private final NullHandling nullHandling;

    public Sort(String property, Direction direction) {
        this(property, direction, DEFAULT_IGNORE_CASE, DEFAULT_NULL_HANDLING);
    }

    public Sort(String property, Direction direction, NullHandling nullHandlingHint) {
        this(property, direction, DEFAULT_IGNORE_CASE, nullHandlingHint);
    }

    private Sort(String property, Direction direction, boolean ignoreCase, NullHandling nullHandling) {
        if (!hasText(property)) {
            throw new IllegalArgumentException("Property must not null or empty!");
        }

        this.direction = direction == null ? DEFAULT_DIRECTION : direction;
        this.property = property;
        this.ignoreCase = ignoreCase;
        this.nullHandling = nullHandling;
    }

    public static Sort by(String property) {
        return new Sort(property, DEFAULT_DIRECTION);
    }

    public static Sort by(String property, Direction direction) {
        return new Sort(property, direction);
    }

    public static Sort asc(String property) {
        return new Sort(property, Direction.ASC, DEFAULT_NULL_HANDLING);
    }

    public static Sort desc(String property) {
        return new Sort(property, Direction.DESC, DEFAULT_NULL_HANDLING);
    }

    private static boolean hasText(String str) {
        return str != null && !str.isEmpty() && containsText(str);
    }

    private static boolean containsText(CharSequence str) {
        int strLen = str.length();

        for (int i = 0; i < strLen; ++i) {
            if (!Character.isWhitespace(str.charAt(i))) {
                return true;
            }
        }

        return false;
    }

    public Direction getDirection() {
        return direction;
    }

    public String getProperty() {
        return property;
    }

    public boolean isAscending() {
        return this.direction.isAscending();
    }

    public boolean isDescending() {
        return this.direction.isDescending();
    }

    public boolean isIgnoreCase() {
        return ignoreCase;
    }

    public Sort with(Direction direction) {
        return new Sort(this.property, direction, this.ignoreCase, this.nullHandling);
    }

    public Sort withProperty(String property) {
        return new Sort(property, this.direction, this.ignoreCase, this.nullHandling);
    }

    public Sort ignoreCase() {
        return new Sort(property, direction, true, nullHandling);
    }

    public Sort with(NullHandling nullHandling) {
        return new Sort(this.property, direction, ignoreCase, nullHandling);
    }

    public Sort nullsFirst() {
        return with(NullHandling.NULLS_FIRST);
    }

    public Sort nullsLast() {
        return with(NullHandling.NULLS_LAST);
    }

    public Sort nullsNative() {
        return with(NullHandling.NATIVE);
    }

    public NullHandling getNullHandling() {
        return nullHandling;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + direction.hashCode();
        result = 31 * result + property.hashCode();
        result = 31 * result + (ignoreCase ? 1 : 0);
        result = 31 * result + nullHandling.hashCode();

        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Sort)) {
            return false;
        }
        Sort that = (Sort) obj;
        return this.direction.equals(that.direction) && this.property.equals(that.property)
                && this.ignoreCase == that.ignoreCase && this.nullHandling.equals(that.nullHandling);
    }

    @Override
    public String toString() {
        String result = String.format("%s: %s", property, direction);

        if (!NullHandling.NATIVE.equals(nullHandling)) {
            result += ", " + nullHandling;
        }
        if (ignoreCase) {
            result += ", ignoring case";
        }

        return result;
    }

    @SuppressWarnings("AlibabaEnumConstantsMustHaveComment")
    public static enum Direction {
        ASC, DESC;

        public static Direction fromString(String value) {
            try {
                return Direction.valueOf(value.toUpperCase(Locale.US));
            } catch (Exception e) {
                throw new IllegalArgumentException(String.format(
                        "Invalid value '%s' for orders given! Has to be either 'desc' or 'asc' (case insensitive).", value), e);
            }
        }

        public static Optional<Direction> fromOptionalString(String value) {
            try {
                return Optional.of(fromString(value));
            } catch (IllegalArgumentException e) {
                return Optional.empty();
            }
        }

        @JsonCreator
        public static Direction convert(@JsonProperty("code") String code) {
            if (Objects.nonNull(code)) {
                final Direction[] values = Direction.values();
                for (Direction value : values) {
                    if (value.name().equalsIgnoreCase(code)) {
                        return value;
                    }
                }
            }
            return null;
        }

        public boolean isAscending() {
            return this.equals(ASC);
        }

        public boolean isDescending() {
            return this.equals(DESC);
        }
    }

    /**
     * Enumeration for null handling hints that can be used in {@link Sort} expressions.
     *
     * @author Thomas Darimont
     * @since 1.8
     */
    public static enum NullHandling {
        /**
         * Lets the data store decide what to do with nulls.
         */
        NATIVE,
        /**
         * A hint to the used data store to order entries with null values before non null entries.
         */
        NULLS_FIRST,

        /**
         * A hint to the used data store to order entries with null values after non null entries.
         */
        NULLS_LAST;
    }
}